﻿using GitCommands;
using GitCommands.Settings;
using GitExtensions.Extensibility;
using GitExtUtils;
using GitExtUtils.GitUI;
using GitExtUtils.GitUI.Theming;
using GitUI.Editor.Diff;
using GitUI.Theming;
using ICSharpCode.TextEditor;
using ICSharpCode.TextEditor.Document;

namespace GitUI.Editor
{
    public partial class FileViewerInternal : GitModuleControl, IFileViewer
    {
        /// <summary>
        /// Raised when the Escape key is pressed (and only when no selection exists, as the default behaviour of escape is to clear the selection).
        /// </summary>
        public event Action? EscapePressed;

        public event EventHandler<SelectedLineEventArgs>? SelectedLineChanged;
        public new event MouseEventHandler? MouseMove;
        public new event EventHandler? MouseEnter;
        public new event EventHandler? MouseLeave;
        public new event System.Windows.Forms.KeyEventHandler? KeyUp;
        public new event EventHandler? DoubleClick;

        private readonly FindAndReplaceForm _findAndReplaceForm = new();
        private readonly CurrentViewPositionCache _currentViewPositionCache;
        private DiffViewerLineNumberControl _lineNumbersControl;
        private ITextHighlightService _textHighlightService = TextHighlightService.Instance;
        private bool _shouldScrollToTop = false;
        private bool _shouldScrollToBottom = false;
        private readonly int _bottomBlankHeight = DpiUtil.Scale(300);
        private ContinuousScrollEventManager? _continuousScrollEventManager;
        private BlameAuthorMargin? _authorsAvatarMargin;
        private bool _showGutterAvatars;

        public FileViewerInternal()
        {
            InitializeComponent();
            InitializeComplete();

            Disposed += (sender, e) =>
            {
                //// _textHighlightService not disposable
                //// _lineNumbersControl not disposable
                //// _currentViewPositionCache not disposable
                _findAndReplaceForm.Dispose();
            };

            FirstVisibleLine = 0;
            _currentViewPositionCache = new CurrentViewPositionCache(this);
            TextEditor.ActiveTextAreaControl.TextArea.SelectionManager.SelectionChanged += SelectionManagerSelectionChanged;

            TextEditor.ActiveTextAreaControl.TextArea.PreviewKeyDown += (s, e) =>
            {
                if (e.KeyCode == Keys.Escape && !TextEditor.ActiveTextAreaControl.SelectionManager.HasSomethingSelected)
                {
                    EscapePressed?.Invoke();
                }
            };

            TextEditor.TextChanged += (s, e) => TextChanged?.Invoke(s, e);
            TextEditor.ActiveTextAreaControl.HScrollBar.ValueChanged += (s, e) => OnHScrollPositionChanged(EventArgs.Empty);
            TextEditor.ActiveTextAreaControl.VScrollBar.ValueChanged += (s, e) => OnVScrollPositionChanged(EventArgs.Empty);
            TextEditor.ActiveTextAreaControl.TextArea.KeyUp += (s, e) => KeyUp?.Invoke(s, e);
            TextEditor.ActiveTextAreaControl.TextArea.DoubleClick += (s, e) => DoubleClick?.Invoke(s, e);
            TextEditor.ActiveTextAreaControl.TextArea.MouseMove += (s, e) => MouseMove?.Invoke(s, e);
            TextEditor.ActiveTextAreaControl.TextArea.MouseEnter += (s, e) => MouseEnter?.Invoke(s, e);
            TextEditor.ActiveTextAreaControl.TextArea.MouseLeave += (s, e) => MouseLeave?.Invoke(s, e);
            TextEditor.ActiveTextAreaControl.TextArea.MouseDown += (s, e) =>
            {
                SelectedLineChanged?.Invoke(
                    this,
                    new SelectedLineEventArgs(
                        TextEditor.ActiveTextAreaControl.TextArea.TextView.GetLogicalLine(e.Y)));
            };
            TextEditor.ActiveTextAreaControl.TextArea.MouseWheel += TextArea_MouseWheel;

            HighlightingManager.Manager.DefaultHighlighting.SetColorFor("LineNumbers",
                new HighlightColor(SystemColors.ControlText, SystemColors.Control, false, false));
            TextEditor.ActiveTextAreaControl.TextEditorProperties.EnableFolding = false;

            _lineNumbersControl = new DiffViewerLineNumberControl(TextEditor.ActiveTextAreaControl.TextArea);

            VRulerPosition = AppSettings.DiffVerticalRulerPosition;
        }

        public void SetContinuousScrollManager(ContinuousScrollEventManager continuousScrollEventManager)
        {
            _continuousScrollEventManager = continuousScrollEventManager;
        }

        private void SelectionManagerSelectionChanged(object sender, EventArgs e)
        {
            string text = TextEditor.ActiveTextAreaControl.TextArea.SelectionManager.SelectedText;
            TextEditor.Document.MarkerStrategy.RemoveAll(m => true);

            IList<TextMarker> selectionMarkers = GetTextMarkersMatchingWord(text);

            foreach (TextMarker selectionMarker in selectionMarkers)
            {
                TextEditor.Document.MarkerStrategy.AddMarker(selectionMarker);
            }

            _textHighlightService.AddTextHighlighting(TextEditor.Document);
            TextEditor.ActiveTextAreaControl.TextArea.Invalidate();
        }

        /// <summary>
        /// Create a list of TextMarker instances in the Document that match the given text.
        /// </summary>
        /// <param name="word">The text to match.</param>
        private IList<TextMarker> GetTextMarkersMatchingWord(string word)
        {
            if (string.IsNullOrWhiteSpace(word))
            {
                return Array.Empty<TextMarker>();
            }

            List<TextMarker> selectionMarkers = [];

            string textContent = TextEditor.Document.TextContent;
            int indexMatch = -1;
            do
            {
                indexMatch = textContent.IndexOf(word, indexMatch + 1, StringComparison.OrdinalIgnoreCase);
                if (indexMatch >= 0)
                {
                    Color highlightColor = AppColor.HighlightAllOccurences.GetThemeColor();

                    TextMarker textMarker = new(indexMatch,
                        word.Length, TextMarkerType.SolidBlock, highlightColor,
                        ColorHelper.GetForeColorForBackColor(highlightColor));

                    selectionMarkers.Add(textMarker);
                }
            }
            while (indexMatch >= 0 && indexMatch < textContent.Length - 1);

            return selectionMarkers;
        }

        public new Font Font
        {
            get => TextEditor.Font;
            set => TextEditor.Font = value;
        }

        public Action? OpenWithDifftool { get; private set; }

        /// <summary>
        /// Move the file viewer cursor position to the next TextMarker found in the document that matches the AppColor.HighlightAllOccurences.
        /// </summary>
        public void GoToNextOccurrence()
        {
            int offset = TextEditor.ActiveTextAreaControl.TextArea.Caret.Offset;

            List<TextMarker> markers = TextEditor.Document.MarkerStrategy.GetMarkers(offset,
                TextEditor.Document.TextLength - offset);

            TextMarker marker =
                markers.FirstOrDefault(x => x.Offset > offset && x.Color == AppColor.HighlightAllOccurences.GetThemeColor());
            if (marker is not null)
            {
                TextLocation position = TextEditor.ActiveTextAreaControl.TextArea.Document.OffsetToPosition(marker.Offset);
                TextEditor.ActiveTextAreaControl.Caret.Position = position;
            }
        }

        /// <summary>
        /// Move the file viewer cursor position to the previous TextMarker found in the document that matches the AppColor.HighlightAllOccurences.
        /// </summary>
        public void GoToPreviousOccurrence()
        {
            int offset = TextEditor.ActiveTextAreaControl.TextArea.Caret.Offset;

            List<TextMarker> markers = TextEditor.Document.MarkerStrategy.GetMarkers(0, offset);

            TextMarker marker =
                markers.LastOrDefault(x => x.Offset < offset && x.Color == AppColor.HighlightAllOccurences.GetThemeColor());
            if (marker is not null)
            {
                TextLocation position = TextEditor.ActiveTextAreaControl.TextArea.Document.OffsetToPosition(marker.Offset);
                TextEditor.ActiveTextAreaControl.Caret.Position = position;
            }
        }

        public void Find(bool replace)
        {
            _findAndReplaceForm.ShowFor(TextEditor, replace && !IsReadOnly);
            OnVScrollPositionChanged(EventArgs.Empty);
        }

        public async Task FindNextAsync(bool searchForwardOrOpenWithDifftool)
        {
            if (searchForwardOrOpenWithDifftool && OpenWithDifftool is not null && string.IsNullOrEmpty(_findAndReplaceForm.LookFor))
            {
                OpenWithDifftool();
                return;
            }

            await _findAndReplaceForm.FindNextAsync(viaF3: true, !searchForwardOrOpenWithDifftool, "Text not found");
            OnVScrollPositionChanged(EventArgs.Empty);
        }

        #region IFileViewer Members

        public event EventHandler? HScrollPositionChanged;
        public event EventHandler? VScrollPositionChanged;
        public new event EventHandler? TextChanged;

        public void ScrollToTop()
        {
            _shouldScrollToTop = true;
        }

        public void ScrollToBottom()
        {
            _shouldScrollToBottom = true;
        }

        public string GetText()
        {
            return TextEditor.Text;
        }

        public bool? ShowLineNumbers { get; set; }

        /// <summary>
        /// Set plain text in the editor.
        /// </summary>
        /// <param name="text">The text to set in the editor.</param>
        /// <param name="openWithDifftool">The command to open the difftool.</param>
        public void SetText(string text, Action? openWithDifftool)
        {
            SetText(text, openWithDifftool, viewMode: ViewMode.Text, useGitColoring: false, contentIdentification: null);
        }

        /// <summary>
        /// Set plain text in the editor.
        /// </summary>
        /// <param name="text">The text to set in the editor.</param>
        /// <param name="openWithDifftool">The command to open the difftool.</param>
        /// <param name="viewMode">the view viewMode in the file viewer, the kind of info shown</param>
        public void SetText(string text, Action? openWithDifftool, ViewMode viewMode, bool useGitColoring, string? contentIdentification)
        {
            ThreadHelper.ThrowIfNotOnUIThread();

            _currentViewPositionCache.Capture();

            OpenWithDifftool = openWithDifftool;

            // Get the highlight service, possibly clean escape sequences in the text and create highlight.
            _textHighlightService = viewMode switch
            {
                ViewMode.Text => TextHighlightService.Instance,
                ViewMode.Diff or ViewMode.FixedDiff => new PatchHighlightService(ref text, useGitColoring),
                ViewMode.CombinedDiff => new CombinedDiffHighlightService(ref text, useGitColoring),
                ViewMode.Difftastic => new DifftasticHighlightService(ref text),
                ViewMode.RangeDiff => new RangeDiffHighlightService(ref text),
                ViewMode.Grep => new GrepHighlightService(ref text),
                _ => throw new ArgumentException($"Unexpected viewMode: {viewMode}", nameof(viewMode))
            };

            TextEditor.Text = text;
            bool hasLineNumberControl = viewMode.IsPartialTextView();
            _lineNumbersControl.Clear();
            _lineNumbersControl.SetVisibility(hasLineNumberControl);

            if (hasLineNumberControl)
            {
                int index = TextEditor.ActiveTextAreaControl.TextArea.LeftMargins.IndexOf(_lineNumbersControl);
                if (index == -1)
                {
                    TextEditor.ActiveTextAreaControl.TextArea.InsertLeftMargin(0, _lineNumbersControl);
                }

                _textHighlightService.SetLineControl(_lineNumbersControl, TextEditor);
            }

            // important to set after the text was changed
            // otherwise the may be rendering artifacts as noted in #5568
            TextEditor.ShowLineNumbers = ShowLineNumbers ?? !hasLineNumberControl;
            if (ShowLineNumbers.HasValue && !ShowLineNumbers.Value)
            {
                Padding = new Padding(DpiUtil.Scale(5), Padding.Top, Padding.Right, Padding.Bottom);
            }

            TextEditor.Refresh();

            // Restore position if contentIdentification matches the capture
            _currentViewPositionCache.Restore(contentIdentification);

            if (_shouldScrollToBottom || _shouldScrollToTop)
            {
                VScrollBar scrollBar = TextEditor.ActiveTextAreaControl.VScrollBar;
                if (scrollBar.Visible)
                {
                    scrollBar.Value = _shouldScrollToTop ? 0 : Math.Max(0, scrollBar.Maximum - scrollBar.Height - _bottomBlankHeight);
                }

                _shouldScrollToTop = false;
                _shouldScrollToBottom = false;
            }
        }

        protected override void OnPaintBackground(PaintEventArgs e)
        {
            if (ShowLineNumbers.HasValue && !ShowLineNumbers.Value)
            {
                e.Graphics.FillRectangle(SystemBrushes.Window, e.ClipRectangle);
            }
            else
            {
                base.OnPaintBackground(e);
            }
        }

        public void SetHighlighting(string syntax) =>
            SetHighlightingStrategy(HighlightingStrategyFactory.CreateHighlightingStrategy(syntax));

        public void SetHighlightingForFile(string filename)
        {
            IHighlightingStrategy highlightingStrategy;
            if (filename.EndsWith("git-rebase-todo"))
            {
                highlightingStrategy = new RebaseTodoHighlightingStrategy(Module);
            }
            else if (filename.EndsWith("COMMIT_EDITMSG"))
            {
                highlightingStrategy = new CommitMessageHighlightingStrategy(Module);
            }
            else
            {
                highlightingStrategy = HighlightingManager.Manager.FindHighlighterForFile(filename);
            }

            SetHighlightingStrategy(highlightingStrategy);
        }

        private void SetHighlightingStrategy(IHighlightingStrategy highlightingStrategy)
        {
            TextEditor.Document.HighlightingStrategy =
                ThemeModule.Settings.UseSystemVisualStyle
                    ? highlightingStrategy
                    : new ThemeBasedHighlighting(highlightingStrategy);
            TextEditor.Refresh();
        }

        public string GetSelectedText()
        {
            return TextEditor.ActiveTextAreaControl.SelectionManager.SelectedText;
        }

        public int GetSelectionPosition()
        {
            if (TextEditor.ActiveTextAreaControl.SelectionManager.SelectionCollection.Count > 0)
            {
                return TextEditor.ActiveTextAreaControl.SelectionManager.SelectionCollection[0].Offset;
            }

            return TextEditor.ActiveTextAreaControl.Caret.Offset;
        }

        public int GetSelectionLength()
        {
            if (TextEditor.ActiveTextAreaControl.SelectionManager.SelectionCollection.Count > 0)
            {
                return TextEditor.ActiveTextAreaControl.SelectionManager.SelectionCollection[0].Length;
            }

            return 0;
        }

        public void EnableScrollBars(bool enable)
        {
            TextEditor.ActiveTextAreaControl.VScrollBar.Width = 0;
            TextEditor.ActiveTextAreaControl.VScrollBar.Visible = enable;
            TextEditor.ActiveTextAreaControl.TextArea.Dock = DockStyle.Fill;
        }

        /// <summary>
        /// Add text highlighting to the document.
        /// This is primarily used to highlight changed files for diffs.
        /// </summary>
        public void AddTextHighlighting()
        {
            TextEditor.Document.MarkerStrategy.RemoveAll(m => true);
            _textHighlightService.AddTextHighlighting(TextEditor.Document);
        }

        /// <summary>
        /// Get the full prefix for lines with differences.
        /// </summary>
        /// <returns>An array with the prefixes.</returns>
        /// <exception cref="ArgumentException"></exception>
        public string[] GetFullDiffPrefixes()
        {
            if (_textHighlightService is not DiffHighlightService highlightService)
            {
                throw new ArgumentException($"Unexpected highlight service {_textHighlightService.GetType()}, not a diff type.");
            }

            return highlightService.GetFullDiffPrefixes();
        }

        /// <summary>
        /// Check if the line is a search match.
        /// For normal diff, this a added deleted line.
        /// For range-diff, this is a header.
        /// </summary>
        /// <returns>An array with the prefixes.</returns>
        /// <param name="indexInText">The line to check.</param>
        private bool IsSearchMatch(int indexInText)
            => _textHighlightService.IsSearchMatch(_lineNumbersControl, indexInText);

        /// <summary>
        /// Go to next change
        /// For normal diffs, this is the next diff
        /// For range-diff, it is the next block of commit summary header.
        /// </summary>
        /// <param name="contextLines">Number of context lines, to include header for new diff.</param>
        public void GoToNextChange(int contextLines)
        {
            // Skip the file header
            bool hasDiffHeader = _textHighlightService is (PatchHighlightService or CombinedDiffHighlightService);
            int firstValidIndex = hasDiffHeader ? 4 : 0;
            int startIndex = Math.Max(firstValidIndex, LineAtCaret);
            int totalNumberOfLines = TotalNumberOfLines;

            bool emptyLineCheck = false;
            for (int line = startIndex; line < totalNumberOfLines; line++)
            {
                if (IsSearchMatch(line))
                {
                    if (emptyLineCheck)
                    {
                        // Include the header with the (possible) function summary line
                        FirstVisibleLine = Math.Max(line - contextLines - 1, 0);
                        LineAtCaret = line;
                        return;
                    }
                }
                else
                {
                    emptyLineCheck = true;
                }
            }
        }

        public void GoToPreviousChange(int contextLines)
        {
            // Skip the file header
            bool hasDiffHeader = _textHighlightService is (PatchHighlightService or CombinedDiffHighlightService);
            int firstValidIndex = hasDiffHeader ? 4 : 0;
            int startIndex = LineAtCaret;
            while (startIndex > firstValidIndex && IsSearchMatch(startIndex))
            {
                // Go to line before of current diff block
                startIndex--;
            }

            // Find the start of previous diff
            bool emptyLineCheck = false;
            for (int line = startIndex; line >= firstValidIndex; line--)
            {
                if (IsSearchMatch(line) && line > firstValidIndex)
                {
                    emptyLineCheck = true;
                    continue;
                }

                if (!emptyLineCheck)
                {
                    continue;
                }

                if (!IsSearchMatch(line))
                {
                    // Restore to last diff
                    line++;
                }

                // Include the header with the (possible) function summary line
                FirstVisibleLine = Math.Max(line - contextLines - 1, 0);
                LineAtCaret = line;
                return;
            }
        }

        /// <summary>
        /// Copy the the text selected in the editor, filtering out lines starting with the given character.
        /// </summary>
        /// <param name="startChar">The start character to ignore for diffs.</param>
        public void CopyNotStartingWith(char startChar)
        {
            string text = GetSelectedText();
            bool noSelection = false;

            if (string.IsNullOrEmpty(text))
            {
                text = GetText();
                noSelection = true;
            }

            bool isDiff = _textHighlightService is DiffHighlightService;
            if (isDiff)
            {
                // add artificial space if selected text is not starting from line beginning, it will be removed later
                int pos = noSelection ? 0 : GetSelectionPosition();
                string fileText = GetText();

                if (pos > 0 && fileText[pos - 1] != '\n')
                {
                    text = " " + text;
                }

                IEnumerable<string> lines = text.LazySplit('\n')
                    .Where(s => s.Length == 0 || s[0] != startChar || (s.Length > 2 && s[1] == s[0] && s[2] == s[0]));
                int hpos = fileText.IndexOf("\n@@");

                // if header is selected then don't remove diff extra chars
                if (hpos <= pos)
                {
                    char[] specials = [' ', '-', '+'];
                    lines = lines.Select(s => s.Length > 0 && specials.Any(c => c == s[0]) ? s[1..] : s);
                }

                text = string.Join("\n", lines);
            }

            ClipboardUtil.TrySetText(text.AdjustLineEndings(Module.GetEffectiveSettingsByPath("core").GetNullableEnum<AutoCRLFType>("autocrlf")));
        }

        public int HScrollPosition
        {
            get { return TextEditor.ActiveTextAreaControl.HScrollBar?.Value ?? 0; }
            set
            {
                HScrollBar scrollBar = TextEditor.ActiveTextAreaControl.HScrollBar;
                if (scrollBar is null)
                {
                    return;
                }

                int max = scrollBar.Maximum - scrollBar.LargeChange;
                max = Math.Max(max, scrollBar.Minimum);
                scrollBar.Value = max > value ? value : max;
            }
        }

        public int VScrollPosition
        {
            get { return TextEditor.ActiveTextAreaControl.VScrollBar?.Value ?? 0; }
            set
            {
                VScrollBar scrollBar = TextEditor.ActiveTextAreaControl.VScrollBar;
                if (scrollBar is null)
                {
                    return;
                }

                int max = scrollBar.Maximum - scrollBar.LargeChange;
                max = Math.Max(max, scrollBar.Minimum);
                scrollBar.Value = max > value ? value : max;
            }
        }

        public EolMarkerStyle EolMarkerStyle
        {
            get => TextEditor.EolMarkerStyle;
            set => TextEditor.EolMarkerStyle = value;
        }

        public bool ShowSpaces
        {
            get => TextEditor.ShowSpaces;
            set => TextEditor.ShowSpaces = value;
        }

        public bool ShowTabs
        {
            get => TextEditor.ShowTabs;
            set => TextEditor.ShowTabs = value;
        }

        public int VRulerPosition
        {
            get => TextEditor.VRulerRow;
            set => TextEditor.VRulerRow = value;
        }

        private int FirstVisibleLine
        {
            get => TextEditor.ActiveTextAreaControl.TextArea.TextView.FirstVisibleLine;
            set => TextEditor.ActiveTextAreaControl.TextArea.TextView.FirstVisibleLine = value;
        }

        public int GetLineFromVisualPosY(int visualPosY)
        {
            return TextEditor.ActiveTextAreaControl.TextArea.TextView.GetLogicalLine(visualPosY);
        }

        private string GetLineText(int line)
        {
            if (line >= TextEditor.Document.TotalNumberOfLines)
            {
                return string.Empty;
            }

            return TextEditor.Document.GetText(TextEditor.Document.GetLineSegment(line));
        }

        public void GoToLine(int lineNumber)
        {
            TextEditor.ActiveTextAreaControl.Caret.Position = new TextLocation(0, GetCaretOffset(lineNumber, rightFile: true));
            TextEditor.ActiveTextAreaControl.CenterViewOn(TextEditor.ActiveTextAreaControl.Caret.Position.Line, treshold: 5);
        }

        /// <summary>
        /// Convert the line number to the offset for the caret.
        /// </summary>
        /// <param name="lineNumber">The line number seen in the editor.</param>
        /// <param name="rightFile">If the line number for the right file is preferred.</param>
        /// <returns>The caret offset</returns>
        private int GetCaretOffset(int lineNumber, bool rightFile)
        {
            if (TextEditor.ShowLineNumbers)
            {
                // All contents is shown, just translate the lineNumber (that is an offset)
                return lineNumber - 1;
            }
            else
            {
                for (int offset = 0; offset < TotalNumberOfLines; ++offset)
                {
                    DiffLineInfo? diffLineNum = _lineNumbersControl.GetLineInfo(offset);
                    if (diffLineNum is not null)
                    {
                        int diffLine = rightFile ? diffLineNum.RightLineNumber : diffLineNum.LeftLineNumber;
                        if (diffLine != DiffLineInfo.NotApplicableLineNum && diffLine >= lineNumber)
                        {
                            return offset;
                        }
                    }
                }
            }

            return 0;
        }

        public int MaxLineNumber => TextEditor.ShowLineNumbers ? TotalNumberOfLines : _lineNumbersControl.MaxLineNumber;

        public int CurrentFileLine()
        {
            bool isPartial = _textHighlightService is (DiffHighlightService or GrepHighlightService or DifftasticHighlightService);
            _currentViewPositionCache.Capture();
            return _currentViewPositionCache.CurrentFileLine(isPartial);
        }

        private int LineAtCaret
        {
            get => TextEditor.ActiveTextAreaControl.Caret.Position.Line;
            set => TextEditor.ActiveTextAreaControl.Caret.Position = new TextLocation(TextEditor.ActiveTextAreaControl.Caret.Position.Column, value);
        }

        public void HighlightLines(int startLine, int endLine, Color color)
        {
            IDocument document = TextEditor.Document;
            if (startLine > endLine || endLine >= document.TotalNumberOfLines)
            {
                return;
            }

            MarkerStrategy markerStrategy = document.MarkerStrategy;
            LineSegment startLineSegment = document.GetLineSegment(startLine);
            LineSegment endLineSegment = document.GetLineSegment(endLine);
            markerStrategy.AddMarker(new TextMarker(startLineSegment.Offset,
                                                    endLineSegment.Offset - startLineSegment.Offset + endLineSegment.Length,
                                                    TextMarkerType.SolidBlock, color));
        }

        public void ClearHighlighting()
        {
            IDocument document = TextEditor.Document;
            document.MarkerStrategy.RemoveAll(t => true);
        }

        public int TotalNumberOfLines => TextEditor.Document.TotalNumberOfLines;

        public bool IsReadOnly
        {
            get => TextEditor.IsReadOnly;
            set => TextEditor.IsReadOnly = value;
        }

        public void SetFileLoader(GetNextFileFnc fileLoader)
        {
            _findAndReplaceForm.SetFileLoader(fileLoader);
        }

        private void TextArea_MouseWheel(object sender, MouseEventArgs e)
        {
            bool isScrollingTowardTop = e.Delta > 0;
            bool isScrollingTowardBottom = e.Delta < 0;
            VScrollBar scrollBar = TextEditor.ActiveTextAreaControl.VScrollBar;

            if (isScrollingTowardTop && (scrollBar.Value == 0))
            {
                _continuousScrollEventManager?.RaiseTopScrollReached(sender, e);
            }

            if (isScrollingTowardBottom && (!scrollBar.Visible || scrollBar.Value + scrollBar.Height > scrollBar.Maximum))
            {
                _continuousScrollEventManager?.RaiseBottomScrollReached(sender, e);
            }
        }

        private void OnHScrollPositionChanged(EventArgs e)
        {
            HScrollPositionChanged?.Invoke(this, e);
        }

        private void OnVScrollPositionChanged(EventArgs e)
        {
            VScrollPositionChanged?.Invoke(this, e);
        }

        #endregion

        public void SetGitBlameGutter(IEnumerable<GitBlameEntry> gitBlameEntries)
        {
            if (_showGutterAvatars)
            {
                _authorsAvatarMargin?.Initialize(gitBlameEntries);
            }
        }

        public bool ShowGutterAvatars
        {
            get => _showGutterAvatars;
            set
            {
                _showGutterAvatars = value;
                if (!_showGutterAvatars)
                {
                    _authorsAvatarMargin?.SetVisiblity(false);

                    return;
                }

                if (_authorsAvatarMargin is null)
                {
                    _authorsAvatarMargin = new BlameAuthorMargin(TextEditor.ActiveTextAreaControl.TextArea);
                    TextEditor.ActiveTextAreaControl.TextArea.InsertLeftMargin(0, _authorsAvatarMargin);
                }
                else
                {
                    _authorsAvatarMargin.SetVisiblity(true);
                }
            }
        }

        internal sealed class CurrentViewPositionCache
        {
            private readonly FileViewerInternal _viewer;
            public string? _currentIdentification;
            public string? _capturedIdentification;
            private ViewPosition _currentViewPosition;

            public CurrentViewPositionCache(FileViewerInternal viewer)
            {
                _viewer = viewer;
            }

            public void Capture()
            {
                if (_viewer.TotalNumberOfLines <= 1 || string.IsNullOrEmpty(_currentIdentification))
                {
                    return;
                }

                // store the previous view position
                _capturedIdentification = _currentIdentification;
                ViewPosition currentViewPosition = new()
                {
                    ActiveLineNum = null,
                    FirstLine = _viewer.GetLineText(0),
                    TotalNumberOfLines = _viewer.TotalNumberOfLines,
                    CaretPosition = _viewer.TextEditor.ActiveTextAreaControl.Caret.Position,
                    FirstVisibleLine = _viewer.FirstVisibleLine
                };
                currentViewPosition.CaretVisible = currentViewPosition.CaretPosition.Line >= currentViewPosition.FirstVisibleLine &&
                                                   currentViewPosition.CaretPosition.Line < currentViewPosition.FirstVisibleLine + _viewer.TextEditor.ActiveTextAreaControl.TextArea.TextView.VisibleLineCount;

                if (_viewer.TextEditor.ShowLineNumbers)
                {
                    // Full contents is shown, just set the line number
                    _currentViewPosition = currentViewPosition;
                    return;
                }

                int initialActiveLine = currentViewPosition.CaretVisible ?
                                            currentViewPosition.CaretPosition.Line :
                                            currentViewPosition.FirstVisibleLine;

                // search downwards for a text line, i.e. a line with line numbers
                int activeLine = initialActiveLine;
                while (activeLine < currentViewPosition.TotalNumberOfLines && currentViewPosition.ActiveLineNum is null)
                {
                    SetActiveLineNum(activeLine);
                    ++activeLine;
                }

                // if none found, search upwards
                activeLine = initialActiveLine - 1;
                while (activeLine >= 0 && currentViewPosition.ActiveLineNum is null)
                {
                    SetActiveLineNum(activeLine);
                    --activeLine;
                }

                _currentViewPosition = currentViewPosition;
                return;

                void SetActiveLineNum(int line)
                {
                    currentViewPosition.ActiveLineNum = _viewer._lineNumbersControl.GetLineInfo(line);
                    if (currentViewPosition.ActiveLineNum is null)
                    {
                        return;
                    }

                    if (currentViewPosition.ActiveLineNum.LeftLineNumber == DiffLineInfo.NotApplicableLineNum &&
                        currentViewPosition.ActiveLineNum.RightLineNumber == DiffLineInfo.NotApplicableLineNum)
                    {
                        currentViewPosition.ActiveLineNum = null;
                    }
                }
            }

            public void Restore(string? contentIdentification)
            {
                _currentIdentification = contentIdentification;
                if (_viewer.TotalNumberOfLines <= 1 || string.IsNullOrEmpty(contentIdentification) || string.IsNullOrEmpty(_currentIdentification))
                {
                    return;
                }

                bool sameIdentification = contentIdentification == _capturedIdentification;
                if (!sameIdentification)
                {
                    return;
                }

                ViewPosition viewPosition = _currentViewPosition;
                if (viewPosition.ActiveLineNum is not null)
                {
                    // prefer the LeftLineNum because the base revision will not change
                    int line = viewPosition.ActiveLineNum.LeftLineNumber != DiffLineInfo.NotApplicableLineNum
                        ? _viewer.GetCaretOffset(viewPosition.ActiveLineNum.LeftLineNumber, rightFile: false)
                        : _viewer.GetCaretOffset(viewPosition.ActiveLineNum.RightLineNumber, rightFile: true);
                    _viewer.TextEditor.ActiveTextAreaControl.Caret.Position = new TextLocation(viewPosition.CaretPosition.Column, line);
                    if (viewPosition.CaretVisible)
                    {
                        _viewer.TextEditor.ActiveTextAreaControl.CenterViewOn(line, treshold: 5);
                    }
                    else
                    {
                        _viewer.FirstVisibleLine = line;
                    }
                }
                else
                {
                    _viewer.FirstVisibleLine = viewPosition.FirstVisibleLine;
                    _viewer.TextEditor.ActiveTextAreaControl.Caret.Position = viewPosition.CaretPosition;
                    if (!viewPosition.CaretVisible)
                    {
                        _viewer.FirstVisibleLine = viewPosition.FirstVisibleLine;
                    }
                }
            }

            /// <summary>
            /// Get the line number at the current view position offset.
            /// </summary>
            /// <param name="isDiff">If the current contents is a file diff.</param>
            /// <returns>The current line number at the caret offset</returns>
            public int CurrentFileLine(bool isDiff)
            {
                ViewPosition viewPosition = _currentViewPosition;
                if (isDiff && _viewer.GetLineText(0) == viewPosition.FirstLine && viewPosition.ActiveLineNum is not null)
                {
                    // prefer the RightLineNum that is for the current revision
                    return viewPosition.ActiveLineNum.RightLineNumber != DiffLineInfo.NotApplicableLineNum
                        ? viewPosition.ActiveLineNum.RightLineNumber
                        : viewPosition.ActiveLineNum.LeftLineNumber;
                }

                // Convert from offset to line number
                return viewPosition.CaretPosition.Line + 1;
            }

            internal TestAccessor GetTestAccessor() => new(this);
            internal readonly struct TestAccessor
            {
                private readonly CurrentViewPositionCache _viewPositionCache;

                public TestAccessor(CurrentViewPositionCache viewPositionCache)
                {
                    _viewPositionCache = viewPositionCache;
                }

                public ViewPosition ViewPosition
                {
                    get => _viewPositionCache._currentViewPosition;
                    set => _viewPositionCache._currentViewPosition = value;
                }

                public TextEditorControl TextEditor => _viewPositionCache._viewer.TextEditor;

                public DiffViewerLineNumberControl LineNumberControl
                {
                    get => _viewPositionCache._viewer._lineNumbersControl;
                    set => _viewPositionCache._viewer._lineNumbersControl = value;
                }

                public void SetCapturedIdentification(string? contentIdentification) => _viewPositionCache._capturedIdentification = contentIdentification;
                public void SetCurrentIdentification(string? contentIdentification) => _viewPositionCache._currentIdentification = contentIdentification;
            }
        }

        internal struct ViewPosition
        {
            internal string FirstLine; // contains the file names in case of a diff
            internal int TotalNumberOfLines; // if changed, CaretPosition and FirstVisibleLine must be ignored and the line number must be searched
            internal TextLocation CaretPosition;
            internal int FirstVisibleLine;
            internal bool CaretVisible; // if not, FirstVisibleLine has priority for restoring
            internal DiffLineInfo? ActiveLineNum;
        }

        internal TestAccessor GetTestAccessor() => new(this);

        internal readonly struct TestAccessor
        {
            private readonly FileViewerInternal _control;

            public TestAccessor(FileViewerInternal control)
            {
                _control = control;
            }

            public TextEditorControl TextEditor => _control.TextEditor;
            public CurrentViewPositionCache CurrentViewPositionCache => _control._currentViewPositionCache;
        }
    }
}
